package redislock

import (
	"strconv"
	"time"

	"gitee.com/zawei/yggdrasil/pkg/log"
	"github.com/go-redis/redis"
)

var (
	luaPexpire = redis.NewScript(`if redislock.call("get", KEYS[1]) == ARGV[1] then return redislock.call("pexpire", KEYS[1], ARGV[2]) else return 0 end`)
	luaDel     = redis.NewScript(`if redislock.call("get", KEYS[1]) == ARGV[1] then return redislock.call("del", KEYS[1]) else return 0 end`)
	luaPTTL    = redis.NewScript(`if redislock.call("get", KEYS[1]) == ARGV[1] then return redislock.call("pttl", KEYS[1]) else return -3 end`)
)

type Lock interface {
	Unlock() error
	TTL() (time.Duration, error)
}

type lock struct {
	unlocked   chan struct{}
	ttl        time.Duration
	cli        redis.Cmdable
	key        string
	value      string
	autoExtend bool
}

func (l *lock) Unlock() error {
	close(l.unlocked)
	res, err := luaDel.Run(l.cli, []string{l.key}, l.value).Result()
	if err == redis.Nil {
		return ErrLockNotHeld
	} else if err != nil {
		return err
	}

	if i, ok := res.(int64); !ok || i != 1 {
		return ErrLockNotHeld
	}
	return nil
}

func (l *lock) TTL() (time.Duration, error) {
	res, err := luaPTTL.Run(l.cli, []string{l.key}, l.value).Result()
	if err == redis.Nil {
		return 0, nil
	} else if err != nil {
		return 0, err
	}

	if num := res.(int64); num > 0 {
		return time.Duration(num) * time.Millisecond, nil
	}
	return 0, nil
}

func (l *lock) apply(opts ...LockOption) {
	for _, o := range opts {
		o(l)
	}
}

func (l *lock) extendTTL() {
	ticker := time.NewTicker(l.ttl / 2)
	for {
		select {
		case <-ticker.C:
			if err := l.refresh(); err != nil {
				log.Warnw("fault to extend lock", "key", l.key, "error", err)
			}
		case <-l.unlocked:
			return
		}
	}

}

func (l *lock) lock() (bool, error) {
	if b, err := l.cli.SetNX(l.key, l.value, l.ttl).Result(); err != nil {
		return false, err
	} else if !b {
		return false, nil
	}
	return true, nil
}

func (l *lock) refresh() error {
	ttlVal := strconv.FormatInt(int64(l.ttl/time.Millisecond), 10)
	status, err := luaPexpire.Run(l.cli, []string{l.key}, l.value, ttlVal).Result()
	if err != nil {
		return err
	} else if status == int64(1) {
		return nil
	}
	return ErrNotObtained
}
